<template>
  <view class="camera">
    <camera class="cameraMode" mode="normal" :flash="flash" device-position="back" resolution="high" frame-size="large" @initdone="handleCameraInit">
      <view class="camera-layer">
        <!-- top -->
        <view class="camera-view-header bg flex-row-center">
          <!-- 提示 -->
          <view class="camera-tips"> {{ tips }}</view>
        </view>
        <!-- center -->
        <view class="camera-view-body">
          <view class="camera-body-left bg"> </view>
          <view class="camera-body">
            <!-- <view class="line">请按照辅助线对准文字</view> -->

            <!-- 标志 -->
            <view class="assist-name">
              <view class="assist-block-bottom"> 请对准要识别的标签 </view>
            </view>
            <!-- 码图 -->
            <view class="assist-code">
              <view class="box-border">
                <view class="border-l"> </view>
                <view class="border-t"> </view>
                <view class="border-r"> </view>
                <view class="border-b"> </view>
              </view>
            </view>
          </view>
          <view class="camera-body-right bg"> </view>
        </view>
        <!-- bottom -->
        <view class="camera-view-footer flex-row-center bg">
          <view class="camera-footer-utils flex-row-center">
            <!-- 拍照 -->
            <view v-if="!auto" class="takePhone iconfont icon-camera" @click="takePhoto"> </view>
            <!-- 闪光灯 -->
            <view class="handle-lamp iconfont" :class="['icon-lamp-' + flash]" @click="lampChange"> </view>
          </view>
        </view>
      </view>
      <!-- 滑动条  && maxZoom > 1" -->
      <view v-if="maxZoom > 1" class="handle-zoom p-20">
        <bar :value="zoom" :max="maxZoom" @change="zoomChange" />
      </view>
      <!-- 关闭 -->
      <view v-if="close" class="handle-colse iconfont icon-close" @click="handleClose"></view>
    </camera>

    <!-- 拍照 -->
    <view v-if="!auto && tempImage" class="imageViewer">
      <image :src="tempImage" mode="widthFix" />
      <view class="imageViewer-option">
        <!-- 放弃 -->
        <view class="imageViewer-item flex-col-center" @click="tempImage = ''">
          <view class="iconfont icon-back"> </view>
          <view class="">重拍 </view>
        </view>
        <!-- 识别 -->
        <view class="imageViewer-item flex-col-center" @click="checkPhoto">
          <view class="iconfont icon-scan"> </view>
          <view class="">识别 </view>
        </view>
      </view>
    </view>

    <!-- canvas -->
    <!-- <canvas type="2d" id="canvas" style="width: 100px; height: 100px; position: fixed; bottom: 200px; border: 1px solid #ccc"> </canvas> -->
    <!-- correctCanvas -->
    <!-- <canvas type="2d" id="correctCanvas" style="width: 100px; height: 100px; position: fixed; bottom: 100px; border: 1px solid #ccc"> </canvas> -->
    <!-- qrCanvas -->
    <canvas type="2d" id="qrCanvas" style="display: none; width: 100px; height: 100px; position: fixed; bottom: 0; border: 1px solid #ccc"> </canvas>
  </view>
</template>

<script>
import jsQR from "jsqr"
import {mapState} from "vuex"
import bar from "@/components/bar/bar"
import {generateGaussianKernel, applyGaussianBlur, calculateSharpness, calculateBrightness, calculateSkewAngles} from "@/utils/algorithm.js"
import {throttle} from "@/utils/index.js"

// 提示定时器
let tipTimer = null

export default {
  name: "Scan",
  components: {
    bar
  },
  props: {
    auto: {
      type: Boolean,
      default: true // 自动查验   手动查验
    },
    mode: {
      type: String,
      default: "normal" //normal 常规    scanCode 扫码
    },
    close: {
      type: Boolean,
      default: true
    },
    configure: {
      type: Object,
      default: () => ({
        iosThreshold: {},
        darkThreshold: {},
        lightThreshold: {}
      })
    }
  },
  computed: {
    ...mapState({
      windowWidth: state => state.configs.windowWidth,
      windowHeight: state => state.configs.windowHeight,
      contentHeight: state => state.configs.contentHeight
    })
  },
  data() {
    return {
      //闪光灯
      flash: "off",
      //相机上下文
      cameraContent: null,
      //监听上下文
      listenContent: null,
      //提示
      tips: "",
      //临时图片
      tempImage: null,
      //相机缩放比例
      maxZoom: 0,
      zoom: 2,
      //canvas
      canvas: null,
      //矫正canvas
      correctCanvas: null,
      //二维码canvas
      qrCanvas: null,
      //裁剪区域
      canvasSize: 0,
      //裁剪X轴
      canvasXAxis: 0,
      //裁剪Y轴
      canvasYAxis: 0,
      //高斯模糊
      kernel: generateGaussianKernel(5, 1.2),
      //暂停获取图像
      pause: true,
      //缩放定时器
      zoomTimer: null
    }
  },
  created() {
    //授权
    wx.authorize({
      scope: ["camera"]
    })
  },

  methods: {
    getDom(id) {
      return new Promise((resolve, reject) => {
        this.createSelectorQuery()
          .select(id)
          .node({node: true, size: true})
          .exec(res => {
            resolve(res[0].node)
          })
      })
    },
    // 暂停获取图像
    stop() {
      this.pause = true

      this.$nextTick(() => {
        this.listenContent && this.listenContent.stop()
      })
    },
    // 恢复获取图像
    restore() {
      this.pause = false

      this.$nextTick(() => {
        // 恢复缩放比例
        if (this.cameraContent) {
          this.zoomChange()
        }

        //开始监听
        this.listenContent && this.listenContent.start()
      })

      uni.hideLoading()
    },
    //相机初始化
    handleCameraInit(e) {
      // 相机已初始化
      if (this.cameraContent) return
      // 创建相机上下文
      this.cameraContent = wx.createCameraContext()

      // 设置缩放比例
      this.maxZoom = e.detail.maxZoom
      this.zoomChange()

      //监听帧
      const callFn = throttle(this.handleFrame, 1000)
      this.listenContent = this.cameraContent.onCameraFrame(res => {
        if (this.pause) return
        else callFn(res)
      })
      this.listenContent && this.listenContent.start()

      console.log("this.cameraContent", this.cameraContent)
    },
    //处理视频帧
    async handleFrame(blurredImageData) {
      console.log("handleFrame", blurredImageData)

      //暂停取图
      this.pause = true

      const imageDate = blurredImageData

      try {
        const {width, height, data} = imageDate

        //1.创建canvas
        if (!this.canvas) {
          // 取图区域大小 取图区域坐标

          // 缩放率
          const scale = width / this.windowWidth
          const sizeWidth = 400 * (this.windowWidth / 750)

          this.canvasSize = sizeWidth * scale
          this.canvasXAxis = (width - this.canvasSize) / 2
          this.canvasYAxis = (height - this.canvasSize) / 2

          this.canvas = wx.createOffscreenCanvas({
            type: "2d",
            width: this.canvasSize,
            height: this.canvasSize
          })

          this.correctCanvas = wx.createOffscreenCanvas({
            type: "2d",
            width: this.canvasSize,
            height: this.canvasSize
          })
          // this.qrCanvas = wx.createOffscreenCanvas({
          //   type: "2d",
          //   width: this.canvasSize,
          //   height: this.canvasSize
          // })
          // this.canvas = await this.getDom("#canvas")
          // this.canvas.width = this.canvasSize
          // this.canvas.height = this.canvasSize

          // this.correctCanvas = await this.getDom("#correctCanvas")
          // this.correctCanvas.width = this.canvasSize
          // this.correctCanvas.height = this.canvasSize

          this.qrCanvas = await this.getDom("#qrCanvas")

          // console.log("canvas", this.canvas, this.correctCanvas, this.qrCanvas)
          console.log("size", this.windowWidth, this.windowHeight, this.contentHeight)
          console.log("canvas", this.canvasSize, this.canvasXAxis, this.canvasYAxis)
        }

        //转成Uint8ClampedArray
        const uint8ClampedArray = new Uint8ClampedArray(data)

        //2.获取canvas上下文 并将取图区域绘制
        const ctx = this.canvas.getContext("2d")

        const formatImageData = ctx.createImageData(width, height)
        formatImageData.data.set(uint8ClampedArray)

        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)

        ctx.putImageData(formatImageData, 0, 0, this.canvasXAxis, this.canvasYAxis, this.canvasSize, this.canvasSize)

        //3.获取待矫正图片
        const imageData = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) || {}

        //4.应用高斯模糊
        const blurredImageData = applyGaussianBlur(imageData, this.kernel)

        //5.检测二维码
        const qrCodeInfo = jsQR(blurredImageData.data, blurredImageData.width, blurredImageData.height)

        if (!qrCodeInfo) {
          return console.error("二维码不存在！")
        }

        //6.获取二维码偏差
        let {correctedTopLeft, qrSize, rotationRadian, verticalSkew, horizontalSkew} = calculateSkewAngles(qrCodeInfo.location, imageData.width, imageData.height)

        //7.角度检测
        if (verticalSkew >= 10 || horizontalSkew >= 10) {
          this.showTips("请保持设备平衡！")
          return console.error(`图片过于倾斜! ${Math.floor(verticalSkew)}*${Math.floor(horizontalSkew)}`)
        }

        //8.有效清理 矫正canvas
        const correctCtx = this.correctCanvas.getContext("2d")
        const qrCtx = this.qrCanvas.getContext("2d")

        correctCtx.clearRect(0, 0, this.correctCanvas.width, this.correctCanvas.height)

        //9.矫正码图
        correctCtx.save()
        correctCtx.translate(this.correctCanvas.width / 2, this.correctCanvas.height / 2)
        correctCtx.rotate(-rotationRadian)
        correctCtx.drawImage(
          this.canvas,
          0,
          0,
          this.correctCanvas.width,
          this.correctCanvas.height,
          -this.correctCanvas.width / 2,
          -this.correctCanvas.height / 2,
          this.correctCanvas.width,
          this.correctCanvas.height
        )
        correctCtx.restore()

        //10.添加间距
        qrSize += 40
        correctedTopLeft.x = correctedTopLeft.x - 20 > 0 ? correctedTopLeft.x - 20 : 0
        correctedTopLeft.y = correctedTopLeft.y - 20 > 0 ? correctedTopLeft.y - 20 : 0

        //11.获取裁剪二维码
        const qrImageData = correctCtx.getImageData(correctedTopLeft.x, correctedTopLeft.y, qrSize, qrSize)

        //12. 亮度检测
        let light = ""
        const brightness = calculateBrightness(qrImageData.data)

        if (brightness < this.configure.darkThreshold.brightnessDown) {
          this.showTips("注意避免光线过暗！")
          return console.error(`图片光线过暗！ ${Math.floor(brightness)}`)
        }
        if (brightness > this.configure.lightThreshold.brightnessUp) {
          this.showTips("注意避免光线过曝！")
          return console.error(`图片光线过曝！ ${Math.floor(brightness)}`)
        }
        if (this.configure.darkThreshold.brightnessDown < brightness < this.configure.darkThreshold.brightnessUp) {
          light = "dark"
        }
        if (this.configure.lightThreshold.brightnessDown < brightness < this.configure.lightThreshold.brightnessUp) {
          light = "light"
        }

        //13.清晰度检测
        const sharpness = calculateSharpness(qrImageData)

        if (light === "dark") {
          if (sharpness < this.configure.darkThreshold.definition) {
            this.showTips("保持设备稳定！")
            return console.error(`图片清晰度不足！ ${Math.floor(sharpness)}`)
          }
        } else {
          if (sharpness < this.configure.lightThreshold.definition) {
            this.showTips("保持设备稳定！")
            return console.error(`图片清晰度不足！ ${Math.floor(sharpness)}`)
          }
        }

        //14.矫正二维码
        this.qrCanvas.width = qrSize
        this.qrCanvas.height = qrSize
        qrCtx.clearRect(0, 0, this.qrCanvas.width, this.qrCanvas.height)
        qrCtx.putImageData(qrImageData, 0, 0)

        //15.返回blob流
        // const image = this.canvas.toDataURL("image/png", 1)

        const tempFilePath = await new Promise((resolve, reject) => {
          wx.canvasToTempFilePath({
            canvas: this.qrCanvas,
            success(res) {
              console.log("res.tempFilePath", res)
              resolve(res.tempFilePath)
            },
            fail(err) {
              console.error("二维码取图！", err)
              reject(err)
            }
          })
        })
        this.$emit("check", tempFilePath)

        console.log("二维码矫正成功！")
      } catch (error) {
        console.error("二维码矫正失败！", error)
      } finally {
        // 恢复取图
        this.pause = false
      }
    },
    // 闪光灯切换
    lampChange() {
      console.log("lamp Change", this.flash)
      let res = ["off", "torch"]
      let index = res.indexOf(this.flash)
      if (index != -1) {
        this.flash = index + 1 == res.length ? res[0] : res[index + 1]
      } else {
        this.flash = "auto"
      }
    },
    // 缩放切换
    zoomChange(zoom) {
      let that = this

      if (that.zoomTimer) {
        clearTimeout(that.zoomTimer)
        that.zoomTimer = null
        return
      }

      that.zoomTimer = setTimeout(() => {
        console.log("相机缩放 sj", that.cameraContent, that.cameraContent.setZoom)

        that.cameraContent &&
          that.cameraContent.setZoom({
            zoom: zoom || that.zoom,
            success() {
              console.log("相机缩放成功！", zoom || that.zoom)

              clearTimeout(that.zoomTimer)
              that.zoomTimer = null
            },
            fail(err) {
              console.error("相机缩放失败！", zoom || that.zoom, err)
            }
          })
      }, 50)
    },
    handleClose() {
      this.$emit("close")
    },
    // 显示提示信息
    showTips(txt) {
      this.tips = txt
      //清理
      if (tipTimer) clearTimeout(tipTimer)
      //计时
      tipTimer = setTimeout(() => {
        this.tips = ""
        //清理
        clearTimeout(tipTimer)
      }, 2000)
    }
  }
}
</script>

<style lang="scss" scoped>
@keyframes animate {
  0% {
    color: #ccc;
  }

  100% {
    color: transparent;
  }
}

.box-border {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  box-sizing: content-box;

  .box-border-item.box-border-mini {
    width: 25rpx;
    height: 25rpx;
  }

  .border-l {
    width: 50rpx;
    height: 50rpx;
    position: absolute;
    left: 0;
    top: 0;
    border-width: 2px 0 0 2px;
    border-style: solid;
    border-color: #4d85ff;
  }

  .border-t {
    width: 50rpx;
    height: 50rpx;
    position: absolute;
    top: 0;
    right: 0;
    border-width: 2px 2px 0 0;
    border-style: solid;
    border-color: #4d85ff;
  }

  .border-r {
    width: 50rpx;
    height: 50rpx;
    position: absolute;
    right: 0;
    bottom: 0;
    border-width: 0 2px 2px 0;
    border-style: solid;
    border-color: #4d85ff;
  }

  .border-b {
    width: 50rpx;
    height: 50rpx;
    position: absolute;
    left: 0;
    bottom: 0;
    border-width: 0 0 2px 2px;
    border-style: solid;
    border-color: #4d85ff;
  }
}

.line {
  padding-bottom: 20rpx;
  position: absolute;
  bottom: 10%;
  left: -10%;
  width: 120%;
  border-bottom: 1rpx dashed green;
  text-align: center;
  color: #ccc;
}

.camera-tips {
  font-size: 26rpx;
  color: #ccc;
  animation: animate 2s linear infinite;
}

.takePhone {
  width: 150rpx;
  height: 150rpx;
  line-height: 150rpx;
  text-align: center;
  border: 4rpx solid #fff;
  background-color: rgba(0, 0, 0, 0.35);
  border-radius: 50%;
  font-size: 60rpx;
  color: #fff;
}

.handle-colse {
  position: absolute;
  right: 20rpx;
  top: 40rpx;
  font-size: 60rpx;
  color: #fff;
}

.handle-lamp {
  position: absolute;
  right: 80rpx;
  font-size: 70rpx;
  color: #fff;
}

.handle-zoom {
  position: absolute;
  right: 40rpx;
  top: 200rpx;
}

.camera {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: #fff;
}

.cameraMode {
  position: relative;
  width: 100%;
  height: 100%;
}

.bg {
  background-color: rgba(0, 0, 0, 0.35);
  color: #fff;
}

.camera-layer {
  display: flex;
  flex-direction: column;
  position: absolute;
  width: 100%;
  height: 100%;

  .camera-view-header {
    width: 100%;
    height: calc((100vh - 400rpx) / 2);
  }

  .camera-view-body {
    // margin-top: 40rpx;
    position: relative;
    width: 100%;
    height: 400rpx;
    display: flex;

    .camera-body-left {
      width: 175rpx;
      height: 400rpx;
    }

    .camera-body {
      position: relative;
      width: 400rpx;
      height: 400rpx;
      display: flex;
      flex-direction: column;

      //logo
      .assist-name {
        width: 100%;
        height: 50rpx;
        // height: 250rpx;
        position: absolute;
        top: -50rpx;

        .assist-block-body {
          display: flex;

          .assist-block-left {
            width: 50rpx;
            height: 150rpx;
          }

          .assist-block-body {
            flex: 1;
            position: relative;
            width: 300rpx;
            height: 150rpx;
          }

          .assist-block-right {
            width: 50rpx;
            height: 150rpx;
          }
        }

        .assist-block-top {
          width: 100%;
          height: 50rpx;
          font-size: 26rpx;
          line-height: 50rpx;
          text-align: center;
        }

        .assist-block-bottom {
          width: 100%;
          height: 50rpx;
          font-size: 26rpx;
          line-height: 50rpx;
          text-align: center;
          color: #fff;
        }
      }

      //two code
      .assist-code {
        position: relative;
        width: 400rpx;
        height: 400rpx;
      }
    }

    .camera-body-right {
      width: 175rpx;
      height: 400rpx;
    }
  }

  .camera-view-footer {
    // flex: 1;
    height: calc((100vh - 400rpx) / 2);

    .camera-footer-utils {
      width: 100%;
      position: relative;
    }
  }
}

.imageViewer {
  position: relative;
  width: 100%;
  height: 100%;
  font-size: 0;
  color: #fff;

  .imageViewer-option {
    position: absolute;
    bottom: 120rpx;
    padding: 40rpx 20rpx;
    width: 100%;
    display: flex;
    justify-content: space-around;
    font-size: 32rpx;

    .iconfont {
      font-size: 52rpx;
      color: #fff;
    }
  }
}
</style>
